package com.netx.generics.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.util.Stack;
import java.util.EmptyStackException;
import java.util.Iterator;
import com.netx.generics.basic.IntegrityException;
import com.netx.generics.io.Location.PROTOCOL;
import com.netx.generics.time.Moment;

// A ProtocolImpl provides the needed functions for FileImpl and
// DirectoryImpl to work. This abstract class provides path handling,
// while subclasses must provide the protocol-specific functions for
// working with the supplied file and directory paths.
abstract class ProtocolImpl {

	// TYPE:
	private static final String _illegalChars = "\\:*?\"|<>";

	// ProtocolImpl factory:
	public static ProtocolImpl getProvider(Location location, String path) {
		if(location.getProtocol() == PROTOCOL.FILE) {
			return new ProtocolImplFile(location, path);
		}
		else {
			// TODO support other protocols in the future
			throw new IntegrityException();
		}
	}

	// INSTANCE:
	protected final Location base;
	protected final String path;
	// For FileImpl:
	protected final boolean hadTrailingSlash;
	
	// Through this constructor, sub-classes path's are guaranteed to:
	// - have a well-formed path
	// - path does not refer to a location before the FileSystem's root
	// - path does not have a leading "/"
	// - path does not have a trailing "/" and errors are properly detected
	// - path is transformed to absolute (no "..") form
	// - filesystem has "" for path (allowing it to be added to sub-paths)
	protected ProtocolImpl(Location base, String path) {
		this.base = base;
		this.hadTrailingSlash = path.endsWith("/");
		this.path = _checkPath(path);
	}
	
	public ProtocolImpl getChild(String relativePath) {
		return ProtocolImpl.getProvider(base, path+"/"+relativePath);
	}

	public ProtocolImpl getParent() {
		// We can't just do path+"/..", because it's illegal for file paths.
		// The best option is to get the path before the last "/".
		if(path.indexOf("/") == -1) {
			throw new IntegrityException(path);
		}
		else {
			String parentPath = path.substring(0, path.lastIndexOf("/"));
			return ProtocolImpl.getProvider(base, parentPath);
		}
	}

	public String getPath() {
		return "/"+path;
	}
	
	public String getName() {
		if(path.contains("/")) {
			return path.substring(path.lastIndexOf("/"));
		}
		else {
			return path;
		}
	}
	
	// Providers' callbacks:
	public abstract String getPhysicalPath();
	public abstract boolean exists() throws IOException;
	public abstract boolean isFile() throws IOException;
	public abstract boolean isDirectory() throws IOException;
	public abstract boolean canRead()throws IOException;
	public abstract boolean canWrite() throws IOException;
	public abstract boolean isHidden() throws IOException;
	public abstract void setReadOnly() throws IOException;
	public abstract Moment getLastModified() throws IOException;
	public abstract void setLastModified(Moment moment) throws IOException;
	public abstract long getSize() throws IOException;
	public abstract void renameTo(String name) throws IOException;
	public abstract String[] list() throws IOException;
	public abstract boolean mkdirs() throws IOException;
	public abstract boolean createNewFile() throws IOException;
	public abstract boolean delete() throws IOException;
	public abstract void deleteOnExit() throws IOException;
	public abstract URI toURI();
	public abstract URL toURL();
	public abstract InputStream getInputStream() throws IOException;
	public abstract OutputStream getOutputStream(boolean append) throws IOException;
	// Providers should translate any IOException to generics.io
	// standard IOExceptions. This method should not throw any
	// RuntimeException; unrecognized IOExceptions should be
	// thrown as UnknownIOException. 
	public abstract IOException translate(IOException io, String path);
	// RuntimeExceptions, if possible, should be translated to 
	// generics.io standard IOException. If it is not possible,
	// they shoud be thrown unchanged.
	public abstract IOException translate(RuntimeException re, String path);
	
	private String _checkPath(String path) {
		// FileSystem:
		if(path.equals("") || path.equals("/")) {
			return "";
		}
		else {
			// Check for illegal characters
			for(int i=0; i<_illegalChars.length(); i++) {
				if(path.indexOf(_illegalChars.charAt(i)) != -1) {
					throw new IllegalArgumentException("'"+path+"': malformed path - illegal char '"+_illegalChars.charAt(i)+"'");
				}
			}
			// Remove leading "/":
			if(path.startsWith("/")) {
				path = path.substring(1);
			}
			// Transform path to absolute form:
			if(path.contains("..")) {
				Stack<String> stack = new Stack<String>();
				String[] pathElems = path.split("[/]");
				for(String p : pathElems) {
					if(!p.equals("")) {
						if(p.equals("..")) {
							try {
								stack.pop();
							}
							catch(EmptyStackException ese) {
								throw new IllegalArgumentException("'"+path+"': path cannot refer to a location above root");
							}
						}
						else {
							stack.push(p);
						}
					}
				}
				// Reference to self:
				if(stack.isEmpty()) {
					return "";
				}
				// OK, build final path:
				StringBuilder sb = new StringBuilder();
				Iterator<String> it = stack.iterator();
				// Size must be put in a variable, otherwise
				// it changes as we pop elements.
				int size = stack.size();
				for(int i=1; it.hasNext(); i++) {
					sb.insert(0, stack.pop());
					if(i < size) {
						sb.insert(0, "/");
					}
				}
				return sb.toString();
			}
			else {
				return path;
			}
		}
	}
}
